const { Error } = require('./error');
const { Comment, Token, String, Parenthesis } = require('./token');
const { parse,
    ASTNode, Quote, Cond, Define, SetValue,
    Lambda, Apply, Begin } = require('./parse')

const operators = [
    "+", "-", "*", "/", "%",
    "=", // will automatically translate to ==
    ">=", "==", "===", "<=", ">", "<", "!", "&&", "||", "&", "|", "^"]
const safeMap = {
    "-": "__",
    "?": "_QuestionMark_",
    ">": "_RightArrow_",
    "<": "_LeftArrow_",
    "!": "_Important_",
    "=": "_Equal_",
}
const safeWords = [
    "var", "while", "of", "arguments",
    "case",
]
// return list of strings
function compileAST(ast, context = null) {
    if (ast instanceof Error) {
        return ast
    }
    return compile_(ast, context)
    // return compileList(astlst, context).join(";\n")
}
// return list of strings
function compileList(vs, context, parent, name) {
    const vvs = []
    const vars = []
    for (const v of vs) {
        if (v instanceof Define) {
            vars.push(safeName(v.name))
        }
    }
    if (vars.length > 0) {
        vvs.push("var " + vars.join(","))
    }
    for (let i = 0; i < vs.length; i++) {
        const v = vs[i]
        let vv
        if (i === vs.length - 1) {
            vv = compile_(v, null, parent, name)
        } else {
            vv = compile_(v, null, parent)
        }
        vvs.push(vv)
        if (context !== null && typeof context === "object" && v instanceof Define) {
            vvs.push(`context.${safeName(v.name)}=${safeName(v.name)}`)
        }
    }
    return vvs
}
// return list of strings
function compile_(ast, context, parent, name) {
    if (typeof ast !== "object") {
        return ast.toString()
    } else if (ast instanceof String) {
        return ast.value
    } else if (ast instanceof Token) {
        if (isNumber(ast)) {
            return ast.value
        } else {
            return safeName(ast.value)
        }
    } else if (ast instanceof Quote) {
        return quote(ast.expr)
    } else if (ast instanceof Begin) {
        let cs = compileList(ast.exprs, context)
        // short cut to compatible previous test
        // if (cs.length === 1) {
        //     return cs[0]
        // }
        if (parent === null || parent === undefined) {
            return `${cs.join(";\n")}`
        } else {
            cs[cs.length - 1] = `return ${cs[cs.length - 1]}`
            return `(function(){\n${cs.join(";\n")}})()`
        }
    } else if (ast instanceof Cond) {
        const ifs = ast.condValues.map(function (cv) {
            let cc = compile_(cv[0], ast)
            if (cc === "else") {
                cc = "true"
            }
            let vvs = compileList(cv.slice(1), null, ast, name)
            const last = vvs[vvs.length - 1]
            if (last.indexOf('/*tail recur*/') !== 0) {
                vvs[vvs.length - 1] = `return ${last}`
            } else {
                vvs[vvs.length - 1] = last
            }
            return `if(${cc}){\n${vvs.join(";\n")}}`
        })
        const ifstr = ifs.join(" else ")
        if (parent instanceof Lambda) {
            return ifstr
        }
        return `(function(){${ifstr}})()`
    } else if (ast instanceof Define) {
        const v = compile_(ast.value, null, ast)
        return `${safeName(ast.name)}=${v}`
    } else if (ast instanceof SetValue) {
        const v = compile_(ast.value, null, ast)
        return `${safeName(ast.name)}=${v}`
    } else if (ast instanceof Lambda) {
        let name = ""
        const args = ast.args.map(safeName).join(",")
        let isT = false
        if (parent instanceof Define) {
            name = parent.name
            isT = isTail(ast.exprs, name, args)
        }
        const n = ast.exprs.length
        const stmts = ast.exprs.map(
            (a, i) => i === n - 1 ? compile_(a, null, ast, name, args) : `${compile_(a, ast)}`
        )
        const last = stmts[stmts.length - 1]
        if (!/^if/.test(last)) {
            stmts[stmts.length - 1] = `return ${last}`
        }
        if (isT) {
            return `(function(${args}){\nbegin__:while(true){${stmts.join(";\n")};break;}})`
        } else {
            return `(function(${args}){\n${stmts.join(";\n")}})`
        }
    } else if (ast instanceof Apply) {
        let func
        const args = ast.args.map(compile_)
        if (ast.func instanceof Token) {
            func = ast.func.value
            if (operators.indexOf(func) !== -1) {
                if (func === '=') {
                    func = '=='
                }
                if (args.length === 1) {
                    return `(${func}${args[0]})`
                } else {
                    return `(${args.map((x) => `${x}`).join(func)})`
                }
            } else if (ast.isTail) {
                // tail recur
                return `/*tail recur*/[${ast.argsNameStr}]=[${args.join(",")}];\ncontinue begin__`
                // } else if (func === "load") {
                //     return `var context=load(${args.join(",")});\n`
            }
        }
        func = compile_(ast.func, null)
        return `(${func})(${args.join(",")})`
    }
}
function isTail(al, name, argsNameStr) {
    if (Array.isArray(al)) {
        return isTailNode(al[al.length - 1], name, argsNameStr)
    }
    return isTailNode(al, name, argsNameStr)
}
function isTailNode(ast, name, argsNameStr) {
    if (ast instanceof String) {
        return false
    } else if (ast instanceof Token) {
        return false
    } else if (ast instanceof Quote) {
        return false
    } else if (ast instanceof Cond) {
        const es = ast.condValues.map(function (cv) {
            return cv[cv.length - 1]
        })
        for (let e of es) {
            if (isTailNode(e, name, argsNameStr)) {
                return true
            }
        }
        return false
    } else if (ast instanceof Define) {
        return false
    } else if (ast instanceof Lambda) {
        return false
    } else if (ast instanceof Apply) {
        let func
        if (ast.func instanceof Token) {
            func = ast.func.value
            if (func === name) {
                // tail recur
                ast.isTail = true
                ast.argsNameStr = argsNameStr
                return true
            }
        }
        return false
    }
}
function quote(ast) {
    if (ast instanceof ASTNode) {
        return `[${ast.children().map(quote)}]`
    }
    if (Array.isArray(ast)) {
        return `[${ast.map(quote)}]`
    }
    if (ast instanceof String) {
        return ast.value
    } else if (isNumber(ast)) {
        return ast.value
    } else { // symbol can't work here
        return `new SSymbol("${ast.value}")`
    }
}
function isNumber(ast) {
    const val = ast.value
    return val && val !== "+" && val !== "-" && (/^[\d+-]/.test(val))
}
function safeName(str) {
    if (safeWords.indexOf(str) !== -1) {
        return `_${str}_`
    }
    const s = []
    for (let c of str) {
        if (safeMap[c] !== undefined) {
            s.push(safeMap[c])
        } else {
            s.push(c)
        }
    }
    return s.join("")
}
module.exports = { compileAST };
